/*
 * Libraries for J2ME (Java mobile) portable devices such as cellular phones.
 * Copyright (c) 2005-2006, Moshe Waisberg. All Rights Reserved.
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Contributors can be contacted by electronic mail via the project Web pages:
 * 
 * https://sourceforge.net/projects/j2melib
 * 
 * http://j2melib.sourceforge.net
 * 
 */
package games;

import java.util.Enumeration;
import java.util.Vector;
import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;

import com.alsutton.jabber.JabberBlockListener;
import games.MatrixEvent;

import javax.microedition.lcdui.Font;///
import javax.microedition.lcdui.Graphics;///
import javax.microedition.lcdui.Image; ///???

/**
 * Encapsulates a <code>N * N</code> size game board.
 * 
 * @author Moshe Waisberg
 */
public abstract class GameBoard extends Canvas implements CommandListener, JabberBlockListener, MatrixEventListener {
    
    
    
    /**
     * Empty cell state with no marker.
     */
    public static final short MARKER_NONE = 0;

    /**
     * No decision yet.
     */
    public static final short WIN_NONE = -1;
    /**
     * Tie game (draw).
     */
    public static final short WIN_TIE = 0;
    /**
     * Winner.
     */
    public static final short WIN_WIN = 1;

    //protected final static Color NAVY = new Color(0x000080);

    protected short[][] matrix;
    private int rows;
    private int cols;
    private final Vector cellListeners = new Vector();
    private int focusRow = -1;
    private int focusCol = -1;
    private boolean square; ///
    private int rowHeight;
    private int colWidth;
    private int xOffset;
    private int yOffset;
    private int clientWidth;
    private int clientHeight;
    ///private boolean isColour;
    /** Border thickness. */
    protected int border = 1;
    /** Draw rounded rectangle for focus? */
    protected boolean roundedFocus = true;
    ///private String busyCaption;

    /**
     * Constructs a new board.
     * 
     * @param owner
     *            the owner.
     * @param rows
     *            the number of rows.
     * @param cols
     *            the number of columns.
     * @param square
     *            is each cell square?
     */
    public GameBoard(int rows, int cols, boolean square) {
        super();
        this.rows = rows;
        this.cols = cols;
        addMatrixListener(this);
        clearMatrix();
        setSquare(square);
    }

    /**
     * Get all the cell markers.
     * 
     * @return the array of markers.
     */
    public short[][] getMatrix() {
        return this.matrix;
    }

    /**
     * Set all the cell markers.
     * 
     * @param matrix
     *            the array of markers.
     */
    protected void setMatrix(short[][] matrix) {
        int rows = getRows();
        int cols = getColumns();
        int row;
        int col;
        int minRows;
        int minCols;
        short[] rowSrc;
        short[] rowDst;

        if (this.matrix == null)
            this.matrix = new short[rows][cols];

        if (matrix == null) {
            for (row = 0; row < rows; row++) {
                for (col = 0; col < cols; col++) {
                    this.matrix[row][col] = MARKER_NONE;
                }
            }
        } else {
            minRows = Math.min(this.matrix.length, matrix.length);
            for (row = 0; row < minRows; row++) {
                rowSrc = matrix[row];
                if (rowSrc != null) {
                    rowDst = this.matrix[row];
                    minCols = Math.min(rowSrc.length, rowDst.length);
                    System.arraycopy(rowSrc, 0, rowDst, 0, minCols);
                }
            }
        }
        markDirty();
    }

    /**
     * Clear all the markers.
     */
    public void clearMatrix() {
        setMatrix(null);
    }
    
    /**
     * Get the marker of a cell.
     * 
     * @param row
     *            the row index.
     * @param col
     *            the column index.
     * @return the marker value.
     */
    public short getMatrix(int row, int col) {
        return this.matrix[row][col];
    }

    /**
     * Set the state of a cell.
     * 
     * @param row
     *            the row index.
     * @param col
     *            the column index.
     * @param marker
     *            the marker value.
     */
    public void setMatrix(int row, int col, short marker) {
        this.matrix[row][col] = marker;
        markDirty();
    }

    /**
     * Get the number of vertical columns.
     * 
     * @return the number of columns.
     */
    public int getColumns() {
        return this.cols;
    }

    /**
     * Get the number of horizontal rows.
     * 
     * @return the number of rows.
     */
    public int getRows() {
        return this.rows;
    }

    protected void paintGrid(Graphics g) {
        int thickness = 1; //TODO

        if (thickness <= 0) return;

        int x1 = getClientX();
        int y1 = getClientY();
        int x2 = x1 + getClientWidth() - 1;
        int y2 = y1 + getClientHeight() - 1;
        int x;
        int y;
        int dx = getColumnWidth() + border;
        int dy = getRowHeight() + border;
        g.setColor(0xC0C0C0); //TODO
        g.setStrokeStyle(g.SOLID);

        for (int t = 0; t < thickness; t++) {
            /* Vertical strips. */
            for (x = x1; x <= x2; x += dx)
                g.drawLine(x + t, y1, x + t, y2);

            /* Horizontal strips. */
            for (y = y1; y <= y2; y += dy)
                g.drawLine(x1, y + t, x2, y + t);
        }
    }

    /**
     * Render the focused cell.
     * 
     * @param g
     *            the graphics.
     */
    protected void paintFocus(Graphics g) {
        int row = getFocusRow();
        int col = getFocusColumn();
        int dx = getColumnWidth() + border;
        int dy = getRowHeight() + border;
        int x;
        int y;

        if ((row >= 0) && (col >= 0)) {
            x = getClientX() + (col * dx);
            y = getClientY() + (row * dy);
            g.setColor(0xff0000);
            g.drawRect(x, y, dx, dy);
            if (roundedFocus) {
                g.drawRoundRect(x + 1, y + 1, dx - 2, dy - 2, 5, 5);
                g.drawRoundRect(x + 2, y + 2, dx - 4, dy - 4, 5, 5);
            }
        }
    }
    
    /**
     * Render the cells.
     * 
     * @param g
     *            the graphics.
     */
    protected void paintMatrix(Graphics g) {
        int rows = getRows();
        int cols = getColumns();
        int row;
        int col;
        short marker;
        int x;
        int y;
        int dx = getColumnWidth();
        int dy = getRowHeight();
        int clipX = g.getClipX();
        int clipY = g.getClipY();
        int clipW = g.getClipWidth();
        int clipH = g.getClipHeight();

        for (row = 0, y = getClientY() + border; row < rows; row++, y += dy + border) {
            for (col = 0, x = getClientX() + border; col < cols; col++, x += dx + border) {
                marker = getMatrix(row, col);
                g.setClip(x, y, dx, dy);
                paintCell(g, row, col, marker, x, y, dx, dy);
            }
        }

        g.setClip(clipX, clipY, clipW, clipH);
    }

    /**
     * Render a cell.
     * 
     * @param g
     *            the graphics.
     * @param row
     *            the row index.
     * @param col
     *            the column index.
     * @param marker
     *            the marker to render.
     * @param x
     *            the horizontal co-ordinate of the top-left corner of the cell.
     * @param y
     *            the vertical co-ordinate of the top-left corner of the cell.
     * @param width
     *            the cell width.
     * @param height
     *            the cell height.
     */
    protected abstract void paintCell(Graphics g, int row, int col, short marker, int x, int y, int width, int height);

    protected void paintImpl(Graphics g) {
        /* Clear client canvas. */
        g.setColor(0xffffff); //todo
        g.fillRect(getClientX(), getClientY(), getClientWidth(), getClientHeight());

        if ((getWidth() == 0) || (getHeight() == 0)) return;
        if ((getRows() == 0) || (getColumns() == 0)) return;

        paintGrid(g);
        paintFocus(g);
        paintMatrix(g);
        //if (isBusy()) paintBusy(g); //////todo
    }

    /**
     * Get the internal width of a column.
     * 
     * @return the column width.
     */
    public int getColumnWidth() {
        return colWidth;
    }

    /**
     * Get the internal height of a row.
     * 
     * @return the row height.
     */
    public int getRowHeight() {
        return rowHeight;
    }

    protected void keyReleased(int keyCode) {
        switch (getGameAction(keyCode)) {
        case FIRE:
            fireMatrixSelected(true);
            break;
        }
    }

    /**
     * Add matrix event listener.
     * 
     * @param listener
     *            the listener.
     */
    public void addMatrixListener(MatrixEventListener listener) {
        if (!cellListeners.contains(listener))
            cellListeners.addElement(listener);
    }

    /**
     * Remove matrix event listener.
     * 
     * @param listener
     *            the listener.
     */
    public void removeMatrixListener(MatrixEventListener listener) {
        cellListeners.removeElement(listener);
    }

    /**
     * Notify matrix event listeners that cell was selected.
     * 
     * @param paint
     *            repaint board?
     */
    protected void fireMatrixSelected(boolean paint) {
        int row = getFocusRow();
        int col = getFocusColumn();

        if ((row < 0) || (col < 0))
            return;

        MatrixEvent event = new MatrixEvent(row, col, getMatrix(row, col));
        MatrixEventListener listener;
        for (Enumeration enu = cellListeners.elements(); enu.hasMoreElements();) {
            listener = (MatrixEventListener) enu.nextElement();
            listener.matrixEntrySelected(event);
        }

        if (paint) paint();
    }

    /**
     * Notify matrix event listeners that cell has focus.
     * 
     * @param paint
     *            repaint board?
     */
    protected void fireMatrixGainFocus(boolean paint) {
        int row = getFocusRow();
        int col = getFocusColumn();

        MatrixEvent event = new MatrixEvent(row, col, getMatrix(row, col));
        MatrixEventListener listener;
        for (Enumeration enu = cellListeners.elements(); enu.hasMoreElements();) {
            listener = (MatrixEventListener) enu.nextElement();
            listener.matrixEntryGainFocus(event);
        }

        if (paint) paint();
    }

    /**
     * Notify matrix event listeners that cell lost focus.
     * 
     * @param paint
     *            repaint board?
     */
    protected void fireMatrixLostFocus(boolean paint) {
        int row = getFocusRow();
        int col = getFocusColumn();

        if ((row < 0) || (col < 0))
            return;

        MatrixEvent event = new MatrixEvent(row, col, getMatrix(row, col));
        MatrixEventListener listener;
        for (Enumeration enu = cellListeners.elements(); enu.hasMoreElements();) {
            listener = (MatrixEventListener) enu.nextElement();
            listener.matrixEntryLostFocus(event);
        }

        if (paint) paint();
    }

    /**
     * Set the focus location.
     * 
     * @param focusRow
     *            the row index.
     * @param focusCol
     *            the column index.
     */
    public void setFocusLocation(int focusRow, int focusCol) {
        this.focusRow = focusRow;
        this.focusCol = focusCol;
    }

    /**
     * Set the focus location. This method fires focus events.
     * 
     * @param focusRow
     *            the row index.
     * @param focusCol
     *            the column index.
     * @see #setFocusLocation(int, int)
     */
    public void setFocus(int focusRow, int focusCol) {
        fireMatrixLostFocus(false);
        setFocusLocation(focusRow, focusCol);
        fireMatrixGainFocus(true);
    }

    /**
     * Get the row with current focus.
     * 
     * @return the row index.
     */
    protected int getFocusRow() {
        return focusRow;
    }

    /**
     * Get the column with current focus.
     * 
     * @return the column index.
     */
    protected int getFocusColumn() {
        return focusCol;
    }

    /**
     * Get the type of winning. Performs a terminal test.
     * 
     * @return the winning state.
     */
    public short winningType() {
        return WIN_NONE;
    }

    /**
     * Is each cell square?
     * 
     * @return true if cells are square - false otherwise.
     */
    public boolean isSquare() {
        return this.square;
    }

    /**
     * Set the square cells indicator.
     * 
     * @param square
     *            is the client area square?
     */
    public void setSquare(boolean square) {
        this.square = square;
        adjustBoundaries(square);
    }

    /**
     * Adjust boundaries.
     * 
     * @param square
     *            is the client area square?
     */
    private void adjustBoundaries(boolean square) {
        int width = getWidth();
        int height = getHeight();
        this.rowHeight = (((height - border) / rows) - border);
        this.colWidth = (((width - border) / cols) - border);
        this.clientHeight = ((rowHeight + border) * rows) + border;
        this.clientWidth = ((colWidth + border) * cols) + border;
        if (square) {
            int min = Math.min(rowHeight, colWidth);
            int client = Math.min(clientWidth, clientHeight);
            this.rowHeight = min;
            this.colWidth = min;
            this.clientHeight = client;
            this.clientWidth = client;
        }
        this.xOffset = (width - clientWidth) >> 1;
        this.yOffset = (height - clientHeight) >> 1;
    }

    /**
     * Get the horizontal offset of the client area.
     * 
     * @return the x offset.
     */
    public int getClientX() {
        return this.xOffset;
    }

    /**
     * Get the vertical offset of the client area.
     * 
     * @return the y offset.
     */
    public int getClientY() {
        return this.yOffset;
    }

    /**
     * Get the height of the board that is actually painted.
     * 
     * @return the client height.
     */
    public int getClientHeight() {
        return this.clientHeight;
    }

    /**
     * Get the width of the board that is actually painted.
     * 
     * @return the client width.
     */
    public int getClientWidth() {
        return this.clientWidth;
    }

    /**
     * Get the background colour of each cell.
     * 
     * @return the background colour.
     */
//    public Color getBackgroundColor() {
//        if (bgColor == null)
//            bgColor = Color.WHITE;
//        return bgColor;
//    }

    /**
     * Set the background colour of each cell.
     * 
     * @param bgColor
     *            the background colour.
     */
//    public void setBackgroundColor(Color bgColor) {
//        this.bgColor = bgColor;
//    }

    /**
     * Get the foreground colour of each cell.
     * 
     * @return the foreground colour.
     */
//    public Color getForegroundColor() {
//        if (fgColor == null)
//            fgColor = Color.BLACK;
//        return fgColor;
//    }

    /**
     * Set the foreground colour of each cell.
     * 
     * @param bgColor
     *            the foreground colour.
     */
//    public void setForegroundColor(Color bgColor) {
//        this.fgColor = bgColor;
//    }

    /**
     * Use a grid border?
     * 
     * @return true if border is drawn.
     */
    public boolean useGridBorder() {
        return (border > 0);
    }

    /**
     * Set the indicator to draw grid border. If true, a border of 1-pixel thickness will be drawn, and each cell will be smaller.
     * 
     * @param use
     *            true if border should be drawn.
     */
    /*public void setUseGridBorder(boolean use) {
    	border = 0;
        if (use)
            border = 1;
            
        adjustBoundaries(isSquare());
    }*/ //if false, selection is invisible :))) TODO!!!

    /**
     * Board state has changed and is marked as dirty.
     */
    protected abstract void markDirty();

    

    /**
     * Set the busy caption, such as when loading data.
     * 
     * @param caption
     *            the caption. A <code>null</code> indicates <code>false</code>.
     */
//    public void setBusy(String caption) {
//        this.busyCaption = caption;
//        paint();
//        serviceRepaints();
//    }

    /**
     * Get the busy status.
     * 
     * @return true if the board is busy.
     */
//    public boolean isBusy() {
//        return (busyCaption != null);
//    }

    /**
     * Paint the board with a "busy" status implementation.
     * 
     * @param g
     *            the graphics.
     */
//    protected void paintBusy(Graphics g) {
//        int width = getWidth();
//        int height = getHeight();
//        int width2 = width >> 1;
//        int height2 = height >> 1;
//        Font f = Font.getFont(Font.FACE_PROPORTIONAL, Font.STYLE_BOLD, Font.SIZE_LARGE);
//        int fHeight = f.getHeight();
//        int fHeight2 = fHeight >> 1;
//        int widthCaption = f.stringWidth(busyCaption) + 10;
//        int x = width2 - (widthCaption >> 1);
//
//        /* Draw box. */
//        g.setStrokeStyle(Graphics.SOLID);
//        g.setColor(Color.CYAN.getRGB());
//        g.fillRoundRect(x - 5, height2 - fHeight2 - 5, widthCaption + 10, fHeight + 10, 10, 10);
//        g.setColor(NAVY.getRGB());
//        g.drawRoundRect(x - 5, height2 - fHeight2 - 5, widthCaption + 10, fHeight + 10, 10, 10);
//        g.drawRoundRect(x - 4, height2 - fHeight2 - 4, widthCaption + 8, fHeight + 8, 10, 10);
//
//        /* Draw caption. */
//        g.setColor(NAVY.getRGB());
//        g.setFont(f);
//        g.drawString(busyCaption, width2, height2 + fHeight2, Graphics.BOTTOM | Graphics.HCENTER);
//    }
    
    /**
     * Get the graphics.
     * 
     * @return the graphics.
     */
    public final Graphics getGraphics() {
        Graphics g = getGraphicsBuffer();

        g.setClip(0, 0, getWidth(), getHeight());
        g.setColor(0xFFFFFF);
        g.fillRect(0, 0, getWidth(), getHeight());
        g.setColor(0x000000);
        g.setFont(Font.getDefaultFont());
        g.setStrokeStyle(Graphics.SOLID);

        return g;
    } //todo ???? for what?
    
    private Graphics gBuffer;
    private Image buffer;
    
    /**
     * Get the buffer.
     * 
     * @return the buffered image.
     */
    private Image getBuffer() {
        if (buffer == null) {
            this.buffer = Image.createImage(getWidth(), getHeight());
        }
        return buffer;
    }
    
    /**
     * Get the graphics buffer.
     * 
     * @return the graphics.
     */
    private Graphics getGraphicsBuffer() {
        if (gBuffer == null) {
            this.gBuffer = getBuffer().getGraphics();
        }
        return gBuffer;
    }
    
    public final void paint() {//todo: for what???
        paint(getGraphics());
    }
}
